////////////////////////////////////////////////////////////////////////////////
// Copyright 2010 Michael Schmalle - Teoti Graphix, LLC
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0 
// 
// Unless required by applicable law or agreed to in writing, software 
// distributed under the License is distributed on an "AS IS" BASIS, 
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and 
// limitations under the License
// 
// Author: Michael Schmalle, Principal Architect
// mschmalle at teotigraphix dot com
////////////////////////////////////////////////////////////////////////////////

package fxplugins.registry.restricted
{

import flash.events.EventDispatcher;
import flash.utils.getDefinitionByName;

import fxplugins.core.IComponentInjector;
import fxplugins.core.IComponentMap;
import fxplugins.core.IComponentMapAware;
import fxplugins.core.IDisposable;
import fxplugins.plugin.IPluginMap;
import fxplugins.plugin.Plugin;
import fxplugins.plugin.PluginExtension;
import fxplugins.plugin.PluginExtensionPoint;
import fxplugins.plugin.XMLPluginExtension;
import fxplugins.registry.ExecutableRequest;
import fxplugins.registry.ExtensionRegistryClientType;
import fxplugins.registry.IContributor;
import fxplugins.registry.IExtension;
import fxplugins.registry.IExtensionDescriptor;
import fxplugins.registry.IExtensionPoint;
import fxplugins.registry.IExtensionRegistry;
import fxplugins.registry.events.ExtensionRegistryChangeKind;
import fxplugins.registry.events.ExtensionRegistryEvent;

import mx.collections.ArrayCollection;
import mx.core.FlexGlobals;

/**
 * Aries [Automated runtime injected extension system]
 */
public class ExtensionRegistry extends EventDispatcher 
	implements IExtensionRegistry, IDisposable, IComponentMapAware
{
	//----------------------------------
	//  componentMap
	//----------------------------------
	
	/**
	 * @private
	 */
	private var _componentMap:IComponentMap;
	
	/**
	 * TODO Docme
	 */
	public function get componentMap():IComponentMap
	{
		return _componentMap;
	}
	
	/**
	 * @private
	 */	
	public function set componentMap(value:IComponentMap):void
	{
		if (_componentMap == value)
			return;
		
		_componentMap = value;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Public Static :: Properties
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 */
	private var _nextID:int = -1;
	
	/**
	 * @private
	 */
	internal function nextID():int
	{
		return _nextID++;
	}
	
	/**
	 * @private
	 */
	private var _nextContributorID:int = -1;
	
	/**
	 * @private
	 */
	internal function nextContributorID():int
	{
		return _nextContributorID++;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private :: Variables
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 * list of IBundleContributor
	 */
	private var contributorCollection:ArrayCollection;
	
	/**
	 * @private
	 * list of IExtensionPoint
	 */
	private var extensionPointCollection:ArrayCollection;
	
	/**
	 * @private
	 * list of IExtension, IExtensionPoint, IExtensionDescriptor
	 */
	private var registryClientCollection:ArrayCollection;
	
	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------
	
	/**
	 * Constructor.
	 * @private
	 */
	public function ExtensionRegistry()
	{
		super();
		
		_nextID = 0;
		_nextContributorID = 0;
		
		contributorCollection = new ArrayCollection();
		extensionPointCollection = new ArrayCollection();
		registryClientCollection = new ArrayCollection();
	}
	
	//--------------------------------------------------------------------------
	//
	//  IDisposable API :: Methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @copy com.teotigraphix.core.IDisposable#dispose()
	 */
	public function dispose():void
	{
		_nextID = 0;
		_nextContributorID = 0;
		
		contributorCollection = new ArrayCollection();
		extensionPointCollection = new ArrayCollection();
		registryClientCollection = new ArrayCollection();
	}
	
	//--------------------------------------------------------------------------
	//
	//  IExtensionRegistry API :: Methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#createExecutableExtension()
	 */
	public function createExecutableExtension(contributor:IContributor,
											  className:String):Object
	{		
		var clazz:Class = null;
		
		try {
			clazz = Class(getDefinitionByName(className));
		}
		catch (e:ReferenceError)
		{
			trace("ExtensionRegistry.createExecutableExtension() - " + e.message);
		}
		
		var token:ExecutableRequest = new ExecutableRequest();
		
		FlexGlobals.topLevelApplication.callLater(instanceReady, [token, clazz]);
		
		return token;
	}
	
	/**
	 * TODO This needs to be handled elswhere.
	 */
	private function instanceReady(token:ExecutableRequest, clazz:Class):void
	{
		var instance:Object = new clazz();
		
		var injector:IComponentInjector = _componentMap.utilityMap.
			getUtility(IComponentInjector) as IComponentInjector;
		
		injector.injectInto(instance);
		
		token.setInstance(instance);
		token.apply();
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#addContributor()
	 */
	public function addContributor(client:IContributor):void
	{
		var registryContributor:RegistryContributor = client as RegistryContributor;
		
		if (!registryContributorContains(registryContributor))
		{
			contributorCollection.addItem(client);
		}
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#addExtensionPoint()
	 */
	public function addExtensionPoint(contributor:IContributor,
									  extensionPointID:String,
									  extensionPointName:String):IExtensionPoint
	{
		return internalAddExtensionPoint(contributor, extensionPointID, extensionPointName);
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#addExtensionDescriptor()
	 */
	public function addExtensionDescriptor(contributor:IContributor,
										   parentID:int,
										   name:String,
										   properties:Object):IExtensionDescriptor
	{
		return internalAddExtensionDescriptor(contributor, parentID, name, properties);
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#addExtension()
	 */
	public function addExtension(contributor:IContributor,
								 extensionID:String,
								 extensionPointID:String,
								 extensionName:String):IExtension 
	{
		if (extensionID == null)
		{
			extensionID = "";
		}
		return internalAddExtension(
			contributor, extensionID, extensionPointID, extensionName);
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getContributor()
	 */
	public function getContributor(id:int):IContributor
	{
		for (var i:int = 0; i < contributorCollection.length; i++)
		{
			var element:IContributor = contributorCollection[i] as IContributor;
			if (element.id == id)
				return element;
		}
		
		return null;
	}	
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensionPoint()
	 */
	public function getExtensionPoint(nameSpace:String, 
									  pointName:String = null):IExtensionPoint
	{
		if (nameSpace == null)
		{
			return null;
		}
		
		if (pointName == null)
		{
			return getInternalExtensionPoint(nameSpace);
		}
		
		return getInternalExtensionPoint(nameSpace + "." + pointName);
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensionPoints()
	 */
	public function getExtensionPoints():Array
	{
		var result:Array = [];
		
		for (var i:int = 0; i < extensionPointCollection.length; i++)
		{
			result.push(extensionPointCollection[i]);
		}
		
		return result;
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtension()
	 */
	public function getExtension(extensionID:String,
								 extensionPointID:String = null):IExtension
	{
		// if no epoint is specified, chop it off the extension's id
		if (extensionPointID == null)
		{
			// if the extension is simple, there is no way to resolve 
			// this extension correctly
			if (extensionID.indexOf(".") == -1)
			{
				return null;
			}
			
			extensionPointID = extensionID.substring(0, 
				extensionID.lastIndexOf("."));
			
			extensionID = extensionID.substring(extensionID.lastIndexOf(".") + 1);
		}
		
		// get all extension for the point
		var point:IExtensionPoint = getExtensionPoint(extensionPointID);
		if (point == null)
		{
			return null;
		}
		
		return point.getExtension(extensionID);
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensionFor()
	 */
	public function getExtensionFor(pluginID:String,
									extensionPointName:String,
									extensionID:String):IExtension
	{
		var point:IExtensionPoint = getExtensionPoint(pluginID + "." + extensionPointName);
		if (point == null)
		{
			return null;
		}
		
		return point.getExtension(extensionID);
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensions()
	 */
	public function getExtensions(extensionPointID:String):Array
	{
		var result:Array = [];
		
		for (var i:int = 0; i < extensionPointCollection.length; i++)
		{
			var point:RegistryExtensionPoint = extensionPointCollection[i] as RegistryExtensionPoint;
			if (point.uniqueID == extensionPointID) 
			{
				for (var j:int = 0; j < point.children.length; j++) 
				{
					result.push(getRegistryExtension(point.children[j]));
				}
			}
		}
		
		return result;
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensionDescriptors()
	 */
	public function getExtensionDescriptors(extension:IExtension):Array
	{
		var result:Array = [];
		var children:Array = RegistryClient(extension).children;
		
		for (var i:int = 0; i < children.length; i++)
		{
			result.push(registryClientCollection.getItemAt(children[i] as int));
		}
		
		return result;
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensionDescriptorsFor()
	 */
	public function getExtensionDescriptorsFor(extensionPointID:String):Array
	{
		var result:Array = [];
		
		var extension:Array = getExtensions(extensionPointID);
		
		for each (var ext:RegistryExtension in extension) 
		{
			var items:Array = ext.getExtensionDescriptors();
			if (items is Array)
			{
				result = result.concat(items);
			}
		}
		
		return result;
	}
	
	/**
	 * @copy fxplugins.registry.IExtensionRegistry#getExtensionDescriptorsByID()
	 */
	public function getExtensionDescriptorByID(descriptorID:String, 
												extensionPointID:String):IExtensionDescriptor
	{
		var descriptors:Array = getExtensionDescriptorsFor(extensionPointID);
		if (!descriptors)
			return null;
		
		for (var i:int = 0; i < descriptors.length; i++)
		{
			var element:IExtensionDescriptor = descriptors[i] as IExtensionDescriptor;
			if (element.uid == descriptorID)
				return element;
		}
		
		return null;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Protected :: Methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 */
	protected function internalAddExtensionPoint(contributor:IContributor,
												 identifier:String, 
												 displayName:String):IExtensionPoint
	{
		var internalContributor:RegistryContributor = null;
		var contributorID:int = -1;
		
		// contribution from a class service
		if (contributor == null)
		{
			contributor = createContributor(-1, "ep://" + identifier);
			addContributor(contributor);
		}
		
		internalContributor = RegistryContributor(contributor);
		contributorID = internalContributor.id;
		
		var point:RegistryExtensionPoint = createExtensionPoint(nextID()) as RegistryExtensionPoint;
		
		var uniqueID:String;		
		var namespaceID:String;
		
		var start:int = identifier.lastIndexOf(".");
		if (start == -1) 
		{
			namespaceID = internalContributor.name;
			uniqueID = namespaceID + "." + identifier;
		} 
		else
		{
			namespaceID = identifier.substring(0, start);
			uniqueID = identifier;
		}
		
		point.uniqueID = uniqueID;
		point.namespaceID = namespaceID;
		
		point.label = displayName;
		
		point.contributorID = contributorID;
		
		extensionPointCollection.addItem(point);
		
		addRegistryClient(point);
		
		// find all extensions that implement this point.
		for (var i:int = 0; i < registryClientCollection.length; i++)
		{
			var client:RegistryClient = registryClientCollection[i] as RegistryClient;
			if (client is RegistryExtension)
			{
				var extension:RegistryExtension = client as RegistryExtension;
				if (extension.extensionPointID == point.uniqueID)
				{
					// add the client to the points's children
					if (point.children == null)
					{
						point.children = [];
					}
					
					point.children.push(client.registryID);
					break;
				}
			}
		}
		
		dispatchRegistryChangeEvent(
			ExtensionRegistryChangeKind.ADD,
			ExtensionRegistryClientType.EXTENSION_POINT,
			point);
		
		return point;
	}
	
	/**
	 * @private
	 * eid org.example.plugins.myTraceOneExtension
	 * epid org.example.points.trace
	 * en null
	 */
	protected function internalAddExtension(contributor:IContributor,
											extensionID:String,
											extensionPointID:String, 
											displayName:String):IExtension
	{
		var internalContributor:RegistryContributor = RegistryContributor(contributor);
		var contributorID:int = internalContributor.id;
		
		var extension:RegistryExtension = createExtension(nextID()) as RegistryExtension;
		
		var simpleIdentifier:String;
		var namespaceName:String;
		
		var simpleIdStart:int = extensionID.lastIndexOf( '.' );
		if (simpleIdStart != -1) 
		{
			simpleIdentifier = extensionID.substring( simpleIdStart + 1 );
			namespaceName = extensionID.substring( 0, simpleIdStart );
		} 
		else 
		{
			// if there is no package must be reletive to IConfigurationElement
			simpleIdentifier = extensionID;
			namespaceName = internalContributor.name;
		}
		
		extension.contributorID = contributorID;
		extension.extensionPointID = extensionPointID;
		extension.simpleID = simpleIdentifier;
		extension.namespaceID = namespaceName;
		extension.label = displayName;
		
		var point:RegistryExtensionPoint = getExtensionPoint(
			extensionPointID) as RegistryExtensionPoint;
		
		if (point)
		{
			point.children.push(extension.registryID);
		}
		
		addRegistryClient(extension);
		
		dispatchRegistryChangeEvent(
			ExtensionRegistryChangeKind.ADD,
			ExtensionRegistryClientType.EXTENSION,
			extension);
		
		return extension;
	}
	
	/**
	 * @private
	 */
	protected function internalAddExtensionDescriptor(contributor:IContributor,
													  parentID:int,
													  name:String,
													  properties:Object):IExtensionDescriptor
	{
		var descriptor:RegistryExtensionDescriptor = 
			createExtensionDescriptor(nextID()) as RegistryExtensionDescriptor;
		
		descriptor.contributorID = contributor.id;
		descriptor.parentID = parentID;
		descriptor.name = name;
		descriptor.properties = properties;
		
		addRegistryClient(descriptor);
		
		// the descriptor is specail, it should not be orphaned since we
		// are passing a parentID [IExtension or IExtensionDescriptor]
		
		var duplicate:Boolean = false;
		var parent:RegistryClient = getRegistryClient(parentID);
		if (parent != null)
		{
			if (parent.children == null)
			{
				parent.children = [descriptor.registryID];
			}
			else
			{
				// check for duplicates
				for (var i:int = 0; i < parent.children.length; i++)
				{
					var index:int = parent.children[i] as int;
					if (index == descriptor.registryID)
					{
						duplicate = true;
						break;
					}
				}
			}
		}
		
		if (!duplicate)
		{
			parent.children.push(descriptor.registryID);
		}
		else
		{
			// Log.error("Duplicated descriptor on parentID")
		}
		
		dispatchRegistryChangeEvent(
			ExtensionRegistryChangeKind.ADD,
			ExtensionRegistryClientType.EXTENSION_DESCRIPTOR,
			descriptor);
		
		return descriptor;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Internal :: Methods
	//
	//--------------------------------------------------------------------------
	
	internal function registryContributorContains(client:IContributor):Boolean
	{
		for (var i:int = 0; i < contributorCollection.length; i++)
		{
			var element:IContributor = contributorCollection[i] as IContributor;
			if (element == client)
				return true;
		}
		
		return false;
	}
	
	/**
	 * @private
	 */
	internal function addRegistryClient(client:RegistryClient):void
	{
		if (!containsID(client.registryID)) 
		{
			registryClientCollection.addItem(client);
		}
	}
	
	private function containsID(registryID:int):Boolean
	{
		for (var i:int = 0; i < registryClientCollection.length; i++)
		{
			var client:RegistryClient = registryClientCollection[i] as RegistryClient;
			if (client.registryID == registryID)
				return true;
		}
		
		return false;
	}
	
	/**
	 * @private
	 */
	internal function getRegistryClient(registryID:int):RegistryClient
	{
		return registryClientCollection.getItemAt(registryID) as RegistryClient;
	}
	
	/**
	 * @private
	 */
	internal function getRegistryExtension(registryID:int):IExtension
	{
		var client:RegistryExtension = registryClientCollection.
			getItemAt(registryID) as RegistryExtension;
		
		if (client != null)
			return client;
		
		return null;
	}
	
	/**
	 * @private
	 */
	internal function getRegistryExtensionDescriptor(registryID:int):IExtensionDescriptor
	{
		var client:RegistryExtensionDescriptor = registryClientCollection.
			getItemAt(registryID) as RegistryExtensionDescriptor;
		
		if (client != null)
			return client;
		
		return null;
	}
	
	/**
	 * @private
	 */
	internal function createExtensionPoint(registryID:int):IExtensionPoint
	{
		return new RegistryExtensionPoint(this, registryID, []);
	}
	
	/**
	 * @private
	 */
	internal function createExtension(registryID:int):IExtension
	{
		return new RegistryExtension(this, registryID, []);
	}
	
	/**
	 * @private
	 */
	internal function createExtensionDescriptor(registryID:int):IExtensionDescriptor
	{
		return new RegistryExtensionDescriptor(this, registryID);
	}
	
	/**
	 * @private
	 */	
	internal function createContributor(bundleID:int, name:String):IContributor
	{
		var c:RegistryContributor = new RegistryContributor(nextContributorID(), name);
		c.bundleID = bundleID;
		return c;
	}
	
	
	private function getInternalExtensionPoint(extensionPointID:String):IExtensionPoint
	{
		for (var i:int = 0; i < extensionPointCollection.length; i++)
		{
			var element:IExtensionPoint = extensionPointCollection[i] as IExtensionPoint;
			if (element.uniqueID == extensionPointID)
				return element;
		}
		
		return null;
	}
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	/**
	 * Initializes a <code>PluginMap</code> instance with the extension registry.
	 * 
	 * <p>The method parses the plugin maps' plugins.</p>
	 * <ul>
	 * <li>Create an add a registry contributor.</li>
	 * <li>Parse all plugin map extension points.</li>
	 * <li>Parse all plugin map extensions.</li>
	 * </ul>
	 * 
	 * @param pluginMap A <code>IPluginMap</code> instance containing 
	 * <code>ExtensionPoint</code> or <code>Extension</code> instances to add to
	 * the registry.
	 * 
	 * @throws Error PluginMap's Plugin instances must set a unique 'uid'
	 */
	public function initialize(pluginMap:IPluginMap):void
	{
		var plugins:Array = pluginMap.plugins;
		if (plugins == null)
			return;
		
		for (var i:int = 0; i < plugins.length; i++)
		{
			var plugin:Plugin = plugins[i] as Plugin;
			if (plugin.uid == null || plugin.uid == "")
			{
				throw new Error("PluginMap's Plugin instances must set a unique 'uid'");
			}
			
			// create an IContributor
			var contributor:IContributor = createContributor(i, plugin.uid);
			
			// add the IContributor
			addContributor(contributor);
			
			// foreach extension point, parse and add to registry
			parseExtensionPoints(plugin, contributor);
			
			/// foreach extension, parse and add to registry
			parseExtensions(plugin, contributor);
		}
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private :: Methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 */
	private function parseExtensionPoints(plugin:Plugin, 
										  contributor:IContributor):void
	{
		if (plugin.points == null || plugin.points.length == 0)
			return;
		
		for (var i:int = 0; i < plugin.points.length; i++)
		{
			var point:PluginExtensionPoint = plugin.points[i] as PluginExtensionPoint;
			if (point.uid == null || point.uid == "")
			{
				// no error here just log the error and don't add the point
				continue;
			}
			
			addExtensionPoint(contributor, point.uid, point.name);
		}
	}
	
	/**
	 * @private
	 */
	private function parseExtensions(plugin:Plugin, 
									 contributor:IContributor):void
	{
		if (plugin.extentions == null || plugin.extentions.length == 0)
			return;
		
		for (var i:int = 0; i < plugin.extentions.length; i++)
		{
			if (plugin.extentions[i] is PluginExtension)
			{
				parseExtension(PluginExtension(plugin.extentions[i]), contributor);
			}
			else if (plugin.extentions[i] is XMLPluginExtension)
			{
				parseXMLExtension(XMLPluginExtension(plugin.extentions[i]), contributor);
			}
		}
	}
	
	/**
	 * @private
	 */
	private function parseExtension(extension:PluginExtension, 
									contributor:IContributor):void
	{
		var extensionID:String = extension.uid;
		var extensionPointID:String = extension.point;
		var extensionLabel:String = extension.name;
		
		var registryExtension:RegistryExtension = addExtension(
			contributor, extensionID, extensionPointID, extensionLabel) as RegistryExtension;
		
		if (extension.children != null && extension.children.length > 0)
		{
			registryExtension.children = parseExensionDescriptors(
				extension.children, registryExtension);
		}
	}
	
	/**
	 * @private
	 */
	private function parseXMLExtension(extension:XMLPluginExtension, 
									   contributor:IContributor):void
	{
		var extensionID:String = extension.uid;
		var extensionPointID:String = extension.point;
		var extensionLabel:String = extension.name;
		
		var registryExtension:RegistryExtension = addExtension(
			contributor, extensionID, extensionPointID, extensionLabel) as RegistryExtension;
		
		if (extension.children != null && extension.children.length() > 0)
		{
			registryExtension.children = parseXMLExensionDescriptors(
				extension.children, registryExtension);
		}
	}
	
	/**
	 * @private
	 */
	private function parseExensionDescriptors(children:Array, 
											  extension:RegistryExtension):Array
	{
		return ExtensionParser.parseExensionDescriptors(
			this, children, extension.contributor, extension.registryID);	
	}
	
	/**
	 * @private
	 */
	private function parseXMLExensionDescriptors(children:XMLList, 
												 extension:RegistryExtension):Array
	{
		return ExtensionParser.parseXMLExensionDescriptors(
			this, children, extension.contributor, extension.registryID);	
	}
	
	
	/**
	 * @private
	 * Dispatch asyncronously so the method returns before listeners
	 * are updated
	 */
	private function dispatchRegistryChangeEvent(kind:String,
												 typeKind:int,
												 element:Object):void
	{
		if (hasEventListener(ExtensionRegistryEvent.REGISTRY_CHANGE))
		{
			var e:ExtensionRegistryEvent = 
				new ExtensionRegistryEvent(ExtensionRegistryEvent.REGISTRY_CHANGE,
					kind, typeKind, element);
			FlexGlobals.topLevelApplication.callLater(doDispatch, [e]);
		}
	}
	
	private function doDispatch(event:ExtensionRegistryEvent):void
	{
		dispatchEvent(event);
	}
}
}